package com.nulldev.util.encoding.base;

import java.nio.charset.Charset;

import com.nulldev.util.encoding.BinaryDecoder;
import com.nulldev.util.encoding.BinaryEncoder;
import com.nulldev.util.other.CharsetUtil;
import com.nulldev.util.string.StringsUtil;

public abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder
{
    static final int EOF = -1;
    public static final int MIME_CHUNK_SIZE = 76;
    public static final int PEM_CHUNK_SIZE = 64;
    private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
    private static final int DEFAULT_BUFFER_SIZE = 8192;
    protected static final int MASK_8BITS = 255;
    protected static final byte PAD_DEFAULT = 61;
    @Deprecated
    protected final byte PAD = 61;
    protected final byte pad;
    private final int unencodedBlockSize;
    private final int encodedBlockSize;
    protected final int lineLength;
    private final int chunkSeparatorLength;
    
    protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, final int lineLength, final int chunkSeparatorLength) {
        this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, (byte)61);
    }
    
    protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, final int lineLength, final int chunkSeparatorLength, final byte pad) {
        this.unencodedBlockSize = unencodedBlockSize;
        this.encodedBlockSize = encodedBlockSize;
        final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0;
        this.lineLength = (useChunking ? (lineLength / encodedBlockSize * encodedBlockSize) : 0);
        this.chunkSeparatorLength = chunkSeparatorLength;
        this.pad = pad;
    }
    
    boolean hasData(final Context context) {
        return context.buffer != null;
    }
    
    int available(final Context context) {
        return (context.buffer != null) ? (context.getPos() - context.readPos) : 0;
    }
    
    protected int getDefaultBufferSize() {
        return 8192;
    }
    
    private byte[] resizeBuffer(final Context context) {
        if (context.buffer == null) {
            context.buffer = new byte[this.getDefaultBufferSize()];
            context.setPos(0);
            context.readPos = 0;
        }
        else {
            final byte[] b = new byte[context.buffer.length * 2];
            System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);
            context.buffer = b;
        }
        return context.buffer;
    }
    
    protected byte[] ensureBufferSize(final int size, final Context context) {
        if (context.buffer == null || context.buffer.length < context.getPos() + size) {
            return this.resizeBuffer(context);
        }
        return context.buffer;
    }
    
    int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) {
        if (context.buffer != null) {
            final int len = Math.min(this.available(context), bAvail);
            System.arraycopy(context.buffer, context.readPos, b, bPos, len);
            context.readPos += len;
            if (context.readPos >= context.getPos()) {
                context.buffer = null;
            }
            return len;
        }
        return context.getEof() ? -1 : 0;
    }
    
    protected static boolean isWhiteSpace(final byte byteToCheck) {
        switch (byteToCheck) {
            case 9:
            case 10:
            case 13:
            case 32: {
                return true;
            }
            default: {
                return false;
            }
        }
    }
    
    @Override
    public Object encode(final Object obj) throws Exception {
        if (!(obj instanceof byte[])) {
            throw new Exception("Parameter supplied to Base-N encode is not a byte[]");
        }
        return this.encode((byte[])obj);
    }
    
    public String encodeToString(final byte[] pArray) {
        return StringsUtil.newStringUtf8(this.encode(pArray));
    }
    
    public String encodeAsString(final byte[] pArray) {
        return StringsUtil.newStringUtf8(this.encode(pArray));
    }
    
    @Override
    public Object decode(final Object obj) throws Exception {
        if (obj instanceof byte[]) {
            return this.decode((byte[])obj);
        }
        if (obj instanceof String) {
            return this.decode((String)obj);
        }
        throw new Exception("Parameter supplied to Base-N decode is not a byte[] or a String");
    }
    
    public byte[] decode(final String pArray) {
    	return this.decode(pArray, CharsetUtil.getCharset());
    }
    
    public byte[] decode(final String pArray, final Charset charset) {
        return this.decode(StringsUtil.getBytes(pArray, charset));
    }
    
    @Override
    public byte[] decode(final byte[] pArray) {
        if (pArray == null || pArray.length == 0) {
            return pArray;
        }
        final Context context = new Context();
        this.decode(pArray, 0, pArray.length, context);
        this.decode(pArray, 0, -1, context);
        final byte[] result = new byte[context.getPos()];
        this.readResults(result, 0, result.length, context);
        return result;
    }
    
    @Override
    public byte[] encode(final byte[] pArray) {
        if (pArray == null || pArray.length == 0) {
            return pArray;
        }
        return this.encode(pArray, 0, pArray.length);
    }
    
    public byte[] encode(final byte[] pArray, final int offset, final int length) {
        if (pArray == null || pArray.length == 0) {
            return pArray;
        }
        final Context context = new Context();
        this.encode(pArray, offset, length, context);
        this.encode(pArray, offset, -1, context);
        final byte[] buf = new byte[context.getPos() - context.readPos];
        this.readResults(buf, 0, buf.length, context);
        return buf;
    }
    
    public abstract void encode(final byte[] p0, final int p1, final int p2, final Context p3);
    
    public abstract void decode(final byte[] p0, final int p1, final int p2, final Context p3);
    
    protected abstract boolean isInAlphabet(final byte p0);
    
    public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) {
        for (final byte octet : arrayOctet) {
            if (!this.isInAlphabet(octet) && (!allowWSPad || (octet != this.pad && !isWhiteSpace(octet)))) {
                return false;
            }
        }
        return true;
    }
    
    public boolean isInAlphabet(final String basen) {
        return this.isInAlphabet(StringsUtil.getBytesUtf8(basen), true);
    }
    
    protected boolean containsAlphabetOrPad(final byte[] arrayOctet) {
        if (arrayOctet == null) {
            return false;
        }
        for (final byte element : arrayOctet) {
            if (this.pad == element || this.isInAlphabet(element)) {
                return true;
            }
        }
        return false;
    }
    
    public long getEncodedLength(final byte[] pArray) {
        long len = (pArray.length + this.unencodedBlockSize - 1) / this.unencodedBlockSize * this.encodedBlockSize;
        if (this.lineLength > 0) {
            len += (len + this.lineLength - 1L) / this.lineLength * this.chunkSeparatorLength;
        }
        return len;
    }
}

